<script setup lang="ts">
import { invoke } from '@tauri-apps/api/core';
import { listen, UnlistenFn } from '@tauri-apps/api/event';
import { basename, dirname } from '@tauri-apps/api/path';
import { open } from '@tauri-apps/plugin-dialog';
import { dateUtils } from 'my-ts-core';
import { showErrorNotification, showInfoNotification } from 'my-vue-ui-core/notification';
import { DateFormatConfig } from 'my-web-core/config';
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';

import EditLocalAppDialog from '@/features/home/components/EditLocalAppDialog.vue';
import HomeContent from '@/features/home/components/HomeContent.vue';
import HomeFooter from '@/features/home/components/HomeFooter.vue';
import HomeHeader from '@/features/home/components/HomeHeader.vue';
import LocalApp, { LocalAppWithUI } from '@/features/home/models/local-app';
import LocalSetting from '@/features/home/models/local-setting';

const { t, locale } = useI18n();
const localAppsWithUI = ref<LocalAppWithUI[]>([]);
const filterKeyword = ref('');
const statusInfo = ref('');
const loading = ref(false);
const showAddItemDialog = ref(false);
const localApp = ref<LocalApp>({});

const filteredLocalAppsWithUI = computed<LocalAppWithUI[]>(() => {
  const keyword = filterKeyword.value.trim().toLowerCase();

  if (!keyword) {
    return localAppsWithUI.value;
  }

  return localAppsWithUI.value?.filter((p) => p.title?.toLocaleLowerCase().includes(keyword));
});

const summaryInfo = computed<string>(() => {
  const selectedCount = (localAppsWithUI.value ?? []).reduce(
    (count, item) => (item.isSelected ? count + 1 : count),
    0,
  );

  const pinnedCount = (localAppsWithUI.value ?? []).reduce(
    (count, item) => (item.isPinned ? count + 1 : count),
    0,
  );

  return t('summaryInfo', {
    selectedCount: selectedCount,
    pinnedCount: pinnedCount,
    totalCount: localAppsWithUI.value?.length ?? 0,
  });
});

// Listener of drag & drop and will be unlistened in unmounted phase.
let unlistenDragDropFn: UnlistenFn;

onMounted(() => {
  // Loads existing local settings.
  loadLocalSettings();

  // Sets up listener for drag & drop.
  listenDragDrop();
});

onUnmounted(() => {
  // Unlistens for drag & drop.
  unlistenDragDropFn?.();
});

watch(locale, () => (statusInfo.value = ''));

/**
 * Creates listener on drag & drop.
 */
async function listenDragDrop(): Promise<void> {
  unlistenDragDropFn = await listen('tauri://drag-drop', async (event) => {
    const newAddedPaths = (event.payload as any).paths;

    if (newAddedPaths?.length > 0) {
      const firstPath = newAddedPaths[0];
      const pathType = await invoke('check_path_type', { path: firstPath });

      if (pathType === 'file') {
        openAddLocalAppDialog(firstPath);
      } else if (pathType === 'dir') {
        // Opens file dialog and gets selected file path.
        const path = await open({ defaultPath: firstPath });

        if (path) {
          openAddLocalAppDialog(path);
        }
      }
    }
  });
}

function onKeywordChange(keyword: string): void {
  filterKeyword.value = keyword;
}

/**
 * Loads local settings from the file and initializes the localAppsWithUI array.
 */
async function loadLocalSettings(): Promise<void> {
  loading.value = true;

  try {
    const setting: LocalSetting = await invoke('read_local_settings');

    localAppsWithUI.value =
      setting?.appList?.map((p) => ({
        ...p,
        isSelected: false,
        isHovered: false,
      })) ?? [];
  } finally {
    loading.value = false;
  }
}

/**
 * Opens a file or directory path using the system's default application.
 */
async function openPath(path: String): Promise<void> {
  const result: string = await invoke('open_path', { path: path });

  if (result) {
    showErrorNotification(result);
  }
}

/**
 * Opens all pinned apps in the list.
 */
function runPinnedItems(): void {
  localAppsWithUI.value?.forEach((app) => {
    if (app.isPinned) {
      openPath(app.fileFullName!);
    }
  });
}

/**
 * Saves the current local app settings to the file.
 */
async function saveSetting(): Promise<void> {
  await invoke('save_local_setting', {
    localSetting: { appList: localAppsWithUI.value },
  });

  statusInfo.value = t('settingSaved', {
    dateTime: dateUtils.formatDate(
      dateUtils.getCurrentDate(),
      DateFormatConfig.longDateTimeFormat,
    ),
  });
}

/**
 * Opens dialog for adding a new local app.
 */
async function openAddLocalAppDialog(path: string): Promise<void> {
  // Tries to find if the app already exists in the list by its fileFullName.
  // If it exists, it will be selected and a notification will be shown.
  const matchedApp = localAppsWithUI.value?.find(
    (app) => app.fileFullName?.toLocaleLowerCase() === path.toLocaleLowerCase(),
  );

  if (matchedApp) {
    // Sets all apps unselected.
    localAppsWithUI.value?.forEach((app) => {
      app.isSelected = false;
    });

    // Selects the matched app.
    matchedApp.isSelected = true;

    // Aslo shows the notification.
    showInfoNotification(t('appExists', { appFullName: path }));

    return;
  }

  // Gets the file name from the path.
  const filename = await basename(path);

  // Sets the app title to the file name without extension.
  localApp.value.title = filename.replace(/\.[^/.]+$/, '');

  localApp.value.fileFullName = path;
  localApp.value.filePath = await dirname(path);
  localApp.value.isPinned = false;
  localApp.value.iconBase64 = await invoke('get_file_icon_base64', { filePath: path });

  // Shows the dialog for adding item.
  showAddItemDialog.value = true;
}

/**
 * Opens dialog for editing an existing local app.
 */
function openEditLocalAppDialog(app: LocalApp): void {
  localApp.value = { ...app };

  // Shows the dialog for editing item.
  showAddItemDialog.value = true;
}

/**
 * Confirm action from dialog.
 */
function confirmDialog(title: string): void {
  // Tries to find the app in the list by its fileFullName.
  const matchedApp = localAppsWithUI.value?.find(
    (app) =>
      app.fileFullName?.toLocaleLowerCase() === localApp.value.fileFullName?.toLocaleLowerCase(),
  );

  if (matchedApp) {
    // Updates the matched app's title if found.
    matchedApp.title = title;
  } else {
    // If not found, adds the new app to the list.
    localApp.value.title = title;

    localAppsWithUI.value?.push({
      ...localApp.value,
      isSelected: false,
      isHovered: false,
    });
  }

  // Hides the dialog after adding the item.
  showAddItemDialog.value = false;
}

/**
 * Clears broken apps from the list.
 * A broken app is defined as an app whose file path does not exist.
 */
async function clearBrokenApps(): Promise<void> {
  const brokenAppIndexes: number[] = [];

  if (localAppsWithUI.value) {
    // Prepares the index list of broken apps.
    for (let i = 0; i < localAppsWithUI.value.length; i++) {
      if (localAppsWithUI.value[i].isPathBroken == true) {
        brokenAppIndexes.push(i);
      }
    }

    // When you remove an item from an array, the remaining items shift.
    // By iterating in reverse order, you ensure that the indices of the items you plan to remove do not get changed by the removal of previous items.
    for (let i = brokenAppIndexes.length - 1; i >= 0; i--) {
      localAppsWithUI.value.splice(brokenAppIndexes[i], 1);
    }

    statusInfo.value = t('clearBrokenInfo', { count: brokenAppIndexes.length });
  }
}
</script>

<template>
  <EditLocalAppDialog
    v-model="showAddItemDialog"
    :title="localApp.title"
    @confirm="confirmDialog"
  />
  <div class="main-container">
    <header>
      <HomeHeader
        :loading
        @run-pinned="runPinnedItems"
        @add="openAddLocalAppDialog"
        @reload="loadLocalSettings"
        @clear-broken="clearBrokenApps"
        @keyword-change="onKeywordChange"
        @save="saveSetting"
      />
    </header>
    <HomeContent
      :loading
      :local-apps-with-u-i="filteredLocalAppsWithUI"
      @list-change="(p) => (localAppsWithUI = p)"
      @edit="openEditLocalAppDialog"
    />
    <footer>
      <HomeFooter :summary-info :status-info />
    </footer>
  </div>
</template>

<style scoped>
.main-container {
  display: flex;
  flex-direction: column;
  height: 100vh;
}

header {
  position: sticky;
  top: 0px;
  z-index: 99;
}
</style>
